//*********************************************************
//
//    Copyright (c) Microsoft. All rights reserved.
//    This code is licensed under the Apache License, Version 2.0.
//    THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
//    ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
//    IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
//    PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
//
//*********************************************************

namespace Microsoft.Research.Provenance.ProvenanceDataService
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using Microsoft.Research.ProvenanceSubscriberCommon.Models;
    using Microsoft.Research.DataLayer;
    using Microsoft.Research.eResearch.Common.Linq;
    using System.Collections.ObjectModel;
    using System.Globalization;
    using Microsoft.Research.ScientificWorkflow.TridentUtilities;

    /// <summary>
    /// This class contains logic for saving the data to provenance DB.
    /// </summary>
    public class DataStore
    {
        private static string LastIndex;

        /// <summary>
        /// Provenance DB Connection.
        /// </summary>
        private Connection provenanceConnection;

        /// <summary>
        /// Constructor.
        /// </summary>
        public DataStore()
        {
            DataConnections.InitializeRegistry();

            this.provenanceConnection = DataConnections.ConnectRegistry();
        }

        /// <summary>
        /// This function is used to store the processing status information.
        /// </summary>
        /// <param name="provenanceModel">ProvenanceInformationModel.</param>
        public void StoreProcessingStatus(ProvenanceInformationModel provenanceModel)
        {
            try
            {
                this.ReopenRegistryConnection();
                ProvenanceDetail provenanceDetail = this.GetProvenanceDetail(provenanceModel);

                if (provenanceModel.ProcessingStatus != null)
                {
                    ProcessingStatusModel status = provenanceModel.ProcessingStatus;
                    ProcessingStatus dataModel = ProcessingStatus.Create(provenanceDetail.ProvenanceInfo.ActivityName, this.provenanceConnection);
                    dataModel.Provenance = provenanceDetail;
                    dataModel.Events = status.Event;
                    dataModel.EventOrder = status.EventOrder;
                    dataModel.Details = status.Details;
                    dataModel.PublishedTime = status.Timestamp;
                    dataModel.Save();
                }
            }
            catch (TridentCustomException)
            {
                // Ignore Trident Custom exception, Error has been Logged previously
                // we need to log the error for each message received from the Black Board, since provenance subscriber cannot be stopped for message errors.
            }
            catch (Exception exp)
            {
                TridentCustomException customException = new TridentCustomException(TridentErrorConstants.ErrorNumber1100000010, "Processing Status Storage Error", exp);
                TridentErrorHandler.HandleUIException(customException);
            }
        }

        /// <summary>
        /// This function is used to store the performance information.
        /// </summary>
        /// <param name="provenanceModel">ProvenanceInformationModel</param>
        public void StorePerformance(ProvenanceInformationModel provenanceModel)
        {
            try
            {
                this.ReopenRegistryConnection();
                ProvenanceDetail provenanceDetail = this.GetProvenanceDetail(provenanceModel);

                if (provenanceDetail != null && provenanceModel.PerformanceDetail != null)
                {
                    PerformanceDetailModel perfdetail = provenanceModel.PerformanceDetail;

                    PerformanceDetail performanceDataModel = PerformanceDetail.Create(provenanceDetail.ProvenanceInfo.ActivityName, this.provenanceConnection);
                    performanceDataModel.Provenance = provenanceDetail;
                    if (perfdetail.PerformanceCounterName.Equals(PerformanceCounter.CPU.ToString()))
                    {
                        performanceDataModel.Counter = PerformanceCounter.CPU;
                    }
                    else
                    {
                        performanceDataModel.Counter = PerformanceCounter.Memory;
                    }

                    performanceDataModel.CounterValue = (int)perfdetail.Value;
                    performanceDataModel.PublishedTime = perfdetail.Timestamp;
                    performanceDataModel.Save();
                    // Refresh performanceDataModel
                    performanceDataModel.Refresh();
                    // Stores the processing info stat information.
                    this.StoreProcessingInfoStat(provenanceDetail);
                }
            }
            catch (TridentCustomException)
            {
                // Ignore Trident Custom exception, Error has been Logged previously
                // we need to log the error for each message received from the Black Board, since provenance subscriber cannot be stopped for message errors.
            }
            catch (Exception exp)
            {
                TridentCustomException customException = new TridentCustomException(TridentErrorConstants.ErrorNumber1100000011, "Performance Storage Error", exp);
                TridentErrorHandler.HandleUIException(customException);
            }
        }

        /// <summary>
        /// Stores the data product.
        /// </summary>
        /// <param name="provenanceModel">The provenance model.</param>
        public void StoreDataProduct(ProvenanceInformationModel provenanceModel)
        {
            try
            {
                this.ReopenRegistryConnection();
                ProvenanceDetail provenanceDetail = this.GetProvenanceDetail(provenanceModel);

                if (provenanceDetail != null && provenanceModel.DataProduct != null)
                {
                    DataProductModel dataProductModel = provenanceModel.DataProduct;

                    DataProduct dataProduct = DataProduct.Load(dataProductModel.DataProductId, this.provenanceConnection);

                    DataProductDetail dataProductDetail = DataProductDetail.Create(dataProduct, provenanceDetail, this.provenanceConnection);
                    dataProductDetail.EventOrder = dataProductModel.EventOrder;

                    dataProductDetail.Save();
                }
            }
            catch (TridentCustomException)
            {
                // Ignore Trident Custom exception, Error has been Logged previously
                // we need to log the error for each message received from the Black Board, since provenance subscriber cannot be stopped for message errors. 
            }
            catch (Exception exp)
            {
                TridentCustomException customException = new TridentCustomException(TridentErrorConstants.ErrorNumber1100000012, "Data product Storage Error", exp);
                TridentErrorHandler.HandleUIException(customException);
            }
        }

        /// <summary>
        /// This function is used to store the InputOutput information.
        /// </summary>
        /// <param name="provenanceModel">ProvenanceInformationModel</param>
        public void StoreInputOutput(ProvenanceInformationModel provenanceModel)
        {
            try
            {
                this.ReopenRegistryConnection();
                ProvenanceDetail provenanceDetail = this.GetProvenanceDetail(provenanceModel);

                if (provenanceDetail != null && provenanceModel.InputOutputParameter != null)
                {
                    InputOutputParameterModel inputOutput = provenanceModel.InputOutputParameter;
                    DirectionType currentDirectionType = DirectionType.Input;

                    try
                    {
                        currentDirectionType = (DirectionType)Enum.Parse(typeof(DirectionType), inputOutput.Direction, true);
                    }
                    catch (ArgumentException) { }

                    InputOutput inputOutputDataModel = InputOutput.Create(provenanceDetail.ProvenanceInfo.ActivityName, inputOutput.PropertyType, currentDirectionType, this.provenanceConnection);

                    inputOutputDataModel.Provenance = provenanceDetail;
                    inputOutputDataModel.Type = inputOutput.PropertyType;
                    inputOutputDataModel.EventOrder = inputOutput.EventOrder;
                    inputOutputDataModel.PropertyName = inputOutput.PropertyName;
                    inputOutputDataModel.PropertyValue = inputOutput.Value.ToString();
                    inputOutputDataModel.PublishedTime = inputOutput.Timestamp;
                    inputOutputDataModel.Type = inputOutput.PropertyType;
                    inputOutputDataModel.Save();
                }
            }
            catch (TridentCustomException)
            {
                // Ignore Trident Custom exception, Error has been Logged previously
                // we need to log the error for each message received from the Black Board, since provenance subscriber cannot be stopped for message errors. 
            }
            catch (Exception exp)
            {
                TridentCustomException customException = new TridentCustomException(TridentErrorConstants.ErrorNumber1100000012, "Input Output Storage Error", exp);
                TridentErrorHandler.HandleUIException(customException);
            }
        }

        /// <summary>
        /// This function is used to store the Provenance information.
        /// </summary>
        /// <param name="provenanceModel">ProvenanceInformationModel.</param>
        public void StoreProvenanceInfo(ProvenanceInformationModel provenanceModel)
        {
            try
            {
                this.ReopenRegistryConnection();
                ProvenanceInfo provenanceDataModel = this.GetProvenanceInfo(provenanceModel);
                if (provenanceDataModel == null)
                {
                    provenanceDataModel = CreateProvenanceInfo(provenanceModel);
                }
            }
            catch (TridentCustomException)
            {
                // Ignore Trident Custom exception, Error has been Logged previously
                // we need to log the error for each message received from the Black Board, since provenance subscriber cannot be stopped for message errors.
            }
            catch (Exception exp)
            {
                TridentCustomException customException = new TridentCustomException(TridentErrorConstants.ErrorNumber1100000013, "Provenance Info Storage Error", exp);
                TridentErrorHandler.HandleUIException(customException);
            }
        }

        /// <summary>
        /// Reopens the registry connection.
        /// </summary>
        private void ReopenRegistryConnection()
        {
            if (this.provenanceConnection == null || !this.provenanceConnection.Alive)
            {
                this.provenanceConnection = DataConnections.ConnectRegistry();
            }
        }

        /// <summary>
        /// Creates the provenance detail.
        /// </summary>
        /// <param name="provModel">The provenance model.</param>
        /// <returns></returns>
        private ProvenanceDetail CreateProvenanceDetail(ProvenanceInformationModel provModel, ProvenanceInfo provenanceInfo)
        {
            ProvenanceDetail provenanceDetail = null;
            try
            {
                provenanceDetail = ProvenanceDetail.Create(this.provenanceConnection);

                if (null != provenanceInfo)
                {
                    provenanceDetail.ProvenanceInfo = provenanceInfo;
                }

                if (!string.IsNullOrEmpty(provModel.CurrentIndex))
                {
                    provenanceDetail.CurrentIndex = provModel.CurrentIndex;
                }

                if (provModel.StartEventOrder > 0)
                {
                    provenanceDetail.StartEventOrder = provModel.StartEventOrder;
                }

                if (provModel.EndEventOrder > 0)
                {
                    provenanceDetail.EndEventOrder = provModel.EndEventOrder;
                }

                provenanceDetail.Save();
            }

            catch (Exception exp)
            {
                TridentCustomException customException = new TridentCustomException(TridentErrorConstants.ErrorNumber1100000014, "Provenance Info Storage Error", exp);
                TridentErrorHandler.HandleUIException(customException);
            }
            return provenanceDetail;
        }

        /// <summary>
        /// This function is used to create the provenance information.
        /// </summary>
        /// <param name="provModel">ProvenanceInformationModel.</param>
        /// <returns>ProvenanceInfo.</returns>
        private ProvenanceInfo CreateProvenanceInfo(ProvenanceInformationModel provModel)
        {
            ProvenanceInfo provenanceDataModel = null;
            try
            {
                provenanceDataModel = ProvenanceInfo.Create(provModel.ActivityName, this.provenanceConnection);

                // Set the Label as ProvenanceInfo Name, If activity Name is null
                provenanceDataModel.Name = string.IsNullOrEmpty(provModel.ActivityName) ? provModel.Label : provModel.ActivityName;
                provenanceDataModel.WorkflowName = provModel.WorkflowName;
                provenanceDataModel.ActivityName = provModel.ActivityName;
                provenanceDataModel.JobId = provModel.JobId;
                if (provModel.ExecutionTime != null)
                {
                    provenanceDataModel.ExecutionTime = provModel.ExecutionTime;
                }

                provenanceDataModel.Save();
            }

            catch (Exception exp)
            {
                TridentCustomException customException = new TridentCustomException(TridentErrorConstants.ErrorNumber1100000014, "Provenance Info Storage Error", exp);
                TridentErrorHandler.HandleUIException(customException);
            }
            return provenanceDataModel;
        }

        /// <summary>
        /// This function calculates the minimum, maximum and average of the
        /// provenance performance infomation and Stores performance stats.
        /// </summary>
        /// <param name="provInfo">The prov info.</param>
        private void StoreProcessingInfoStat(ProvenanceDetail provInfo)
        {
            try
            {
                PerformanceDetailCollection<ProvenanceDetail> performanceDetails = provInfo.PerformanceDetails;

                if (provInfo.ProvenanceInfo != null && performanceDetails.Count > 0)
                {
                    PerformanceStatCollection<ProvenanceDetail> PerformanceStats = provInfo.PerformanceStats;
                    double cpuMin, cpuMax, cpuAvg = 0;
                    IEnumerable<PerformanceDetail> activityCPUgroup = performanceDetails
                        .Where(model => model.Counter == PerformanceCounter.CPU); //(IObject)model.
                    if (activityCPUgroup.Count() > 0)
                    {
                        cpuAvg = activityCPUgroup.Average(p => p.CounterValue);
                        cpuMin = activityCPUgroup.Min(p => p.CounterValue);
                        cpuMax = activityCPUgroup.Max(p => p.CounterValue);

                        PerformanceStat cpuPerfomance = PerformanceStats
                            .FirstOrDefault(model => model.Name == PerformanceCounter.CPU.ToString());

                        if (cpuPerfomance == null)
                        {
                            cpuPerfomance = PerformanceStat.Create(PerformanceCounter.CPU.ToString(), this.provenanceConnection);
                            cpuPerfomance.Provenance = provInfo;
                        }
                        cpuPerfomance.Counter = PerformanceCounter.CPU.ToString();
                        cpuPerfomance.Average = cpuAvg.ToString("F3", CultureInfo.InvariantCulture);
                        cpuPerfomance.Minimum = cpuMin.ToString(CultureInfo.InvariantCulture);
                        cpuPerfomance.Maximum = cpuMax.ToString(CultureInfo.InvariantCulture);
                        cpuPerfomance.Save();
                    }

                    IEnumerable<PerformanceDetail> activityMemorygroup = performanceDetails
                      .Where(model => model.Counter == PerformanceCounter.Memory);
                    if (activityMemorygroup.Count() > 0)
                    {
                        double memoryMin = 0, memoryMax = 0, memoryAvg = 0;

                        memoryAvg = activityMemorygroup.Average(p => p.CounterValue);
                        memoryMin = activityMemorygroup.Min(p => p.CounterValue);
                        memoryMax = activityMemorygroup.Max(p => p.CounterValue);

                        PerformanceStat memoryPerfomance = PerformanceStats.FirstOrDefault(
                            model => model.Name == PerformanceCounter.Memory.ToString());
                        if (memoryPerfomance == null)
                        {
                            memoryPerfomance = PerformanceStat.Create(PerformanceCounter.Memory.ToString(), this.provenanceConnection);
                            memoryPerfomance.Provenance = provInfo;
                        }
                        memoryPerfomance.Counter = PerformanceCounter.Memory.ToString();
                        memoryPerfomance.Average = memoryAvg.ToString("F3", CultureInfo.InvariantCulture);
                        memoryPerfomance.Minimum = memoryMin.ToString(CultureInfo.InvariantCulture);
                        memoryPerfomance.Maximum = memoryMax.ToString(CultureInfo.InvariantCulture);
                        memoryPerfomance.Save();
                    }
                }
            }
            catch (Exception exp)
            {
                TridentCustomException customException = new TridentCustomException(TridentErrorConstants.ErrorNumber1100000015, "Provenance statistics Storage Error", exp);
                TridentErrorHandler.HandleUIException(customException);
            }
        }

        /// <summary>
        /// Gets the provenance info.
        /// </summary>
        /// <param name="provenanceModel">Provenance model.</param>
        /// <returns></returns>
        private ProvenanceInfo GetProvenanceInfo(ProvenanceInformationModel provenanceModel)
        {
            ProvenanceInfo provenanceInfo = null;
            try
            {
                ProvenanceInfo.ISearch.ISearchGroup provenanceSearchQuery = null;

                // Activity Name Clause.
                ProvenanceInfo.ISearch.ISearchClause activityNameClause =
                    ProvenanceInfo.ISearch.ActivityName(StringField.Condition.Equals, provenanceModel.ActivityName);

                // JobId Clause.
                ProvenanceInfo.ISearch.ISearchClause jobIDClause =
                    ProvenanceInfo.ISearch.JobId(StringField.Condition.Equals, provenanceModel.JobId.ToString());

                provenanceSearchQuery = ProvenanceInfo.ISearch.AND(activityNameClause, jobIDClause);

                ProvenanceInfo.ISearch provenanceSearch = ProvenanceInfo.ISearch.Create();

                provenanceSearch.Query = provenanceSearchQuery;

                List<ProvenanceInfo> provenanceInfos = ProvenanceInfo.Search(provenanceSearch, this.provenanceConnection);

                // It should return 1 record.
                if (provenanceInfos.Count > 0)
                {
                    provenanceInfo = provenanceInfos[0];
                }
            }
            catch (RecordNotFoundException)
            {
            }

            return provenanceInfo;
        }

        /// <summary>
        /// Gets the provenance detail.
        /// </summary>
        /// <param name="model">The model.</param>
        /// <param name="provenanceInfo">The provenance info.</param>
        /// <returns></returns>
        public ProvenanceDetail GetProvenanceDetail(ProvenanceInfo provenanceInfo)
        {
            ProvenanceDetail provDetail = null;
            try
            {
                ProvenanceDetail.ISearch searchQuery = ProvenanceDetail.ISearch.Create();
                searchQuery.Query = ProvenanceDetail.ISearch.AND(
                    ProvenanceDetail.ISearch.ProvenanceInfo(ObjectFieldBase.Condition.Equals, provenanceInfo),
                    ProvenanceDetail.ISearch.EndEventOrder(IntegerField.Condition.Equals, 0));

                List<ProvenanceDetail> provDetails = ProvenanceDetail.Search(searchQuery, this.provenanceConnection);

                if (provDetails.Count > 0)
                {
                    provDetail = provDetails[0];
                }
            }
            catch (RecordNotFoundException)
            {
            }

            return provDetail;
        }

        /// <summary>
        /// Gets the provenance detail.
        /// </summary>
        /// <param name="provenanceModel">The provenance model.</param>
        /// <returns></returns>
        private ProvenanceDetail GetProvenanceDetail(ProvenanceInformationModel provenanceModel)
        {
            ProvenanceDetail provenanceDetail = null;

            // Get the ProvenanceInfo for Activity Name and JobId.
            ProvenanceInfo provenanceInfo = this.GetProvenanceInfo(provenanceModel);

            // If entry is not present in registry in case of new messages.
            if (provenanceInfo == null)
            {
                provenanceInfo = this.CreateProvenanceInfo(provenanceModel);
                provenanceDetail = this.CreateProvenanceDetail(provenanceModel, provenanceInfo);
            }
            else
            {
                provenanceInfo.ProvenanceDetails.GetEnumerator();
                if (provenanceInfo.ProvenanceDetails.Count > 0)
                {
                    provenanceDetail = provenanceInfo.ProvenanceDetails
                        .FirstOrDefault(provDetail =>
                            CheckCurrentIndex(provenanceModel, provDetail));
                }

                if (provenanceDetail == null ||
                    (null != provenanceModel.CurrentIndex &&
                    !provenanceModel.CurrentIndex.Equals(LastIndex, StringComparison.CurrentCulture)))
                {
                    provenanceDetail = this.CreateProvenanceDetail(provenanceModel, provenanceInfo);
                    LastIndex = provenanceModel.CurrentIndex;
                }
                else
                {
                    DataStore.UpdateProvenanceDetail(provenanceDetail, provenanceModel);
                }
            }

            return provenanceDetail;
        }

        /// <summary>
        /// Checks the index of the current.
        /// </summary>
        /// <param name="provenanceModel">The provenance model.</param>
        /// <param name="provDetail">The prov detail.</param>
        /// <returns></returns>
        private static bool CheckCurrentIndex(ProvenanceInformationModel provenanceModel, ProvenanceDetail provDetail)
        {
            return string.Equals(provDetail.CurrentIndex, provenanceModel.CurrentIndex, StringComparison.OrdinalIgnoreCase);
        }

        /// <summary>
        /// Updates the provenance detail.
        /// </summary>
        /// <param name="provenanceDetail">The provenance detail.</param>
        /// <param name="provModel">The prov model.</param>
        private static void UpdateProvenanceDetail(ProvenanceDetail provenanceDetail, ProvenanceInformationModel provModel)
        {
            try
            {
                if (provModel.StartEventOrder > 0)
                {
                    provenanceDetail.StartEventOrder = provModel.StartEventOrder;
                }

                if (provModel.EndEventOrder > 0)
                {
                    provenanceDetail.EndEventOrder = provModel.EndEventOrder;
                }

                provenanceDetail.Save();
            }

            catch (Exception exp)
            {
                TridentCustomException customException = new TridentCustomException(TridentErrorConstants.ErrorNumber1100000014, "Provenance Info Storage Error", exp);
                TridentErrorHandler.HandleUIException(customException);
            }
        }

    }
}